A chat application enables real-time communication between users through text-based messages. It is commonly used in personal messaging, customer support, collaboration tools, and social networking platforms.
In this chapter, we will explore the low-level design of a simple in-memory chat application.
Let's start by clarifying the requirements:
Before starting the design, it's important to ask thoughtful questions to uncover hidden assumptions and better define the scope of the system.
Here is an example of how a conversation between the candidate and the interviewer might unfold:
Candidate: Should the application support both one-on-one and group chats?
Interviewer: Yes, it should support both types of conversations.
Candidate: Can the users edit or delete message after sending?
Interviewer: No. Once a message is sent, it cannot be modified or deleted.
Candidate: Should users be able to see their chat history?
Interviewer: Yes, the system should store and display full conversation history for each user.
Candidate: Should we support message delivery status indicators, such as sent, delivered, or read?
Interviewer: Not for this version. Just assume messages are delivered once they are sent. No need to track read receipts or delivery confirmations.
Candidate: Should the system preserve the order of messages?
Interviewer: Yes, messages must be delivered in the order they were sent.
After gathering the details, we can summarize the key system requirements.
Core entities are the fundamental building blocks of our system. We identify them by analyzing key nouns (e.g., user, message, chat, session, contact list) and actions (e.g., authenticate, send, receive, display, store) from the functional requirements. These often translate directly into classes, enums, or interfaces in an object-oriented design.
Below, we break down the functional requirements and extract the relevant entities. Related requirements are grouped together when they represent the same conceptual unit.
This suggests the need for a Chat entity that can represent either a one-on-one chat or a group conversation. Each chat has a list of participating users and a collection of messages.
Additionally, we need a ChatService to act as the orchestrator. It will be responsible for creating new chats, adding participants, retrieving chat history, and managing chat-level operations.
This introduces the User entity, which represents each participant in the system. A user can be part of multiple chats and can send or receive messages.
Messages themselves are modeled using the Message entity, which includes sender, timestamp and the message content.
These core entities define the essential abstractions of a simple chat application and will guide the structure of your low-level design and class diagrams.
This section outlines the classes that form the core of the chat application, the relationships between them, and the key design patterns employed to ensure a scalable and maintainable architecture.
The system is defined by a set of core classes and data classes.
MessageThis is an immutable data class representing a single message.
It encapsulates the message id, sender (User), content, and timestamp. Once a Message object is created, its state cannot be changed, which is ideal for representing historical records like chat messages.
UserRepresents a participant in the chat system.
Each User has a unique id and a name. Crucially, it contains the onMessageReceived method, which acts as a callback for the Observer pattern, allowing a user to be "notified" of new messages.
Chat (Abstract Class): This class serves as the blueprint for all types of conversations. It manages a collection of members (User objects) and messages (Message objects). It defines the common behavior for all chats but delegates the specific implementation of retrieving the chat's name to its subclasses via the abstract getName method.
OneToOneChat A concrete implementation of Chat designed for a private conversation between exactly two users.
GroupChatA concrete implementation of Chat for conversations involving multiple users. It includes functionality to add or remove members.
ChatServiceThe central hub of the application.
It manages the lifecycle and registration of all users and chats. It acts as a go-between for all interactions, such as sending messages and creating new chats.
The relationships between classes define the structure and interaction flow of the application.
OneToOneChat and GroupChat both extend the abstract Chat class. This is an "is-a" relationship, where both concrete classes are specialized types of a Chat. They inherit the common state (members, messages) and behavior (addMessage) while providing their own specific implementation for getName.Chat is composed of Messages. A Message cannot exist without being part of a Chat. The Chat class manages the lifecycle of the Message objects within its messages list. This strong "part-of" relationship is a classic example of composition. Chat (Whole) *---Message (Part)Chat aggregates Users as its members. A User can exist independently of any single chat and can be a member of multiple chats simultaneously. This is a "has-a" relationship, but the lifecycle of a User is not tied to the lifecycle of a Chat. Chat (Whole) <>---User (Part)ChatService aggregates all User and Chat objects in the system. It holds references to them in its maps but doesn't exclusively own them in a compositional sense.Message to User. Each Message object holds a reference to the User who sent it. This relationship is essential for identifying the sender of any given message. Message ---> User (sender)Several design patterns are utilized to create a decoupled and organized system.
The ChatService is a textbook implementation of the Mediator pattern. It acts as a central communications hub, preventing User objects from needing to reference each other directly. All communication, like sending a message, is routed through the ChatService. This decouples users and chats, simplifying the system and making it easier to manage and extend. For example, a User sending a message only needs to know the chatId, not the details of all other recipients.
This pattern is used to notify users of new messages.
ChatService acts as the subject. When its sendMessage method is called, it triggers an event (a new message).User class is the observer. Its onMessageReceived method is the callback that is invoked by the ChatService for every member of a chat when a new message is posted. This creates a push-based notification system.The abstract getName(User perspectiveUser) method in the Chat class and its concrete implementations in OneToOneChat and GroupChat exemplify the Strategy pattern. The algorithm (or strategy) for determining a chat's display name varies depending on the chat type.
OneToOneChat Strategy: The name is the name of the other user.GroupChat Strategy: The name is the fixed groupName. This allows the client code to get the appropriate name polymorphically without needing to know the concrete type of the chat.The ChatService class acts as a factory for creating core domain objects. Methods like createUser, createOneToOneChat, and createGroupChat centralize the instantiation logic. This simplifies the creation process for the client and ensures that all created objects are properly registered within the service.
UserRepresents a user in the chat system.
1class User:
2 def __init__(self, name: str):
3 self.id = str(uuid.uuid4())
4 self.name = name
5
6 def get_id(self) -> str:
7 return self.id
8
9 def get_name(self) -> str:
10 return self.name
11
12 def on_message_received(self, message: 'Message', chat_context: 'Chat'):
13 print(f"[Notification for {self.get_name()} in chat '{chat_context.get_name(self)}'] {message.get_sender().get_name()}: {message.get_content()}")
14
15 def __eq__(self, other):
16 if self is other:
17 return True
18 if other is None or type(self) != type(other):
19 return False
20 return self.id == other.id
21
22 def __hash__(self):
23 return hash(self.id)
24
25 def __str__(self):
26 return f"User{{id='{self.id}', name='{self.name}'}}"onMessageReceived() method is called when the user receives a new message in a chat, simulating push notifications.MessageRepresents a single message sent in a chat.
1class Message:
2 def __init__(self, sender: User, content: str):
3 self.id = str(uuid.uuid4())
4 self.sender = sender
5 self.content = content
6 self.timestamp = datetime.now()
7
8 def get_id(self) -> str:
9 return self.id
10
11 def get_sender(self) -> User:
12 return self.sender
13
14 def get_content(self) -> str:
15 return self.content
16
17 def get_timestamp(self) -> datetime:
18 return self.timestamp
19
20 def __str__(self):
21 return f"[{self.timestamp}] {self.sender.get_name()}: {self.content}"Chat (Abstract Class)Abstract base class for all types of chats. Maintains a list of members and messages.
1class Chat(ABC):
2 def __init__(self):
3 self.id = str(uuid.uuid4())
4 self.members: List[User] = []
5 self.messages: List[Message] = []
6 self._lock = Lock()
7
8 def get_id(self) -> str:
9 return self.id
10
11 def get_members(self) -> List[User]:
12 with self._lock:
13 return copy.copy(self.members)
14
15 def get_messages(self) -> List[Message]:
16 with self._lock:
17 return copy.copy(self.messages)
18
19 def add_message(self, message: Message):
20 with self._lock:
21 self.messages.append(message)
22
23 @abstractmethod
24 def get_name(self, perspective_user: User) -> str:
25 passOneToOneChatConcrete chat class for private messaging between two users.
1class OneToOneChat(Chat):
2 def __init__(self, user1: User, user2: User):
3 super().__init__()
4 self.members.extend([user1, user2])
5
6 def get_name(self, perspective_user: User) -> str:
7 for member in self.members:
8 if member != perspective_user:
9 return member.get_name()
10 return "Unknown Chat"GroupChatRepresents a group chat with multiple members.
1class GroupChat(Chat):
2 def __init__(self, group_name: str, initial_members: List[User]):
3 super().__init__()
4 self.group_name = group_name
5 self.members.extend(initial_members)
6
7 def add_member(self, user: User):
8 with self._lock:
9 if user not in self.members:
10 self.members.append(user)
11
12 def remove_member(self, user: User):
13 with self._lock:
14 if user in self.members:
15 self.members.remove(user)
16
17 def get_name(self, perspective_user: User) -> str:
18 return self.group_nameChatService (Mediator)This service class is the heart of the application. It acts as a central Mediator that handles all user and chat management, as well as message routing.
1class ChatService:
2 def __init__(self):
3 self.users: Dict[str, User] = {}
4 self.chats: Dict[str, Chat] = {}
5 self._lock = Lock()
6
7 def create_user(self, name: str) -> User:
8 user = User(name)
9 with self._lock:
10 self.users[user.get_id()] = user
11 return user
12
13 def create_one_to_one_chat(self, user_id1: str, user_id2: str) -> Chat:
14 user1 = self.users.get(user_id1)
15 user2 = self.users.get(user_id2)
16 chat = OneToOneChat(user1, user2)
17 with self._lock:
18 self.chats[chat.get_id()] = chat
19 return chat
20
21 def create_group_chat(self, name: str, member_ids: List[str]) -> Chat:
22 members = []
23 for member_id in member_ids:
24 members.append(self.users.get(member_id))
25 chat = GroupChat(name, members)
26 with self._lock:
27 self.chats[chat.get_id()] = chat
28 return chat
29
30 def send_message(self, sender_id: str, chat_id: str, message_content: str):
31 sender = self.users.get(sender_id)
32 chat = self.chats.get(chat_id)
33
34 if chat is None:
35 print(f"Error: Chat not found with ID: {chat_id}")
36 return
37
38 if sender not in chat.get_members():
39 print(f"Error: Sender {sender.get_name()} is not a member of this chat.")
40 return
41
42 message = Message(sender, message_content)
43 chat.add_message(message)
44
45 # Notify all members of the chat (Observer pattern)
46 for member in chat.get_members():
47 # Do not send a notification to the sender
48 if member != sender:
49 member.on_message_received(message, chat)
50
51 def print_chat_history(self, chat_id: str) -> List[Message]:
52 chat = self.chats.get(chat_id)
53 if chat is not None:
54 return chat.get_messages()
55 return []
56
57 def get_user_chats(self, user_id: str) -> List[Chat]:
58 user = self.users.get(user_id)
59 if user is None:
60 return []
61
62 result = []
63 for chat in self.chats.values():
64 if user in chat.get_members():
65 result.append(chat)
66 return resultChatService is a classic example of the Mediator pattern. Users do not communicate directly with each other; they communicate only through the ChatService. This decouples users and centralizes the complex communication logic, making the system easier to manage and extend.
This method orchestrates the entire process of sending a message:
This driver class demonstrates the end-to-end functionality of the system, acting as a client to the ChatService.
1class ChatApplicationDemo:
2 @staticmethod
3 def main():
4 # 1. Initialize the Mediator (ChatService)
5 chat_service = ChatService()
6
7 # 2. Create and register users
8 alice = chat_service.create_user("Alice")
9 bob = chat_service.create_user("Bob")
10 charlie = chat_service.create_user("Charlie")
11
12 print("--- Users registered in the system ---")
13 print()
14
15 # 3. Scenario 1: One-on-one chat between Alice and Bob
16 print("--- Starting one-on-one chat between Alice and Bob ---")
17 alice_bob_chat = chat_service.create_one_to_one_chat(alice.get_id(), bob.get_id())
18
19 # Alice sends a message to Bob
20 print("Alice sends a message...")
21 chat_service.send_message(alice.get_id(), alice_bob_chat.get_id(), "Hi Bob, how are you?")
22
23 # Bob sends a reply
24 print("\nBob sends a reply...")
25 chat_service.send_message(bob.get_id(), alice_bob_chat.get_id(), "I'm good, Alice! Thanks for asking.")
26 print()
27
28 # 4. Scenario 2: Group chat
29 print("--- Starting a group chat for a 'Project Team' ---")
30 project_members = [alice.get_id(), bob.get_id(), charlie.get_id()]
31 project_group = chat_service.create_group_chat("Project Team", project_members)
32
33 # Charlie sends a message to the group
34 print("Charlie sends a message to the group...")
35 chat_service.send_message(charlie.get_id(), project_group.get_id(), "Hey team, when is our deadline?")
36
37 # Alice replies to the group
38 print("\nAlice replies to the group...")
39 chat_service.send_message(alice.get_id(), project_group.get_id(), "It's next Friday. Let's sync up tomorrow.")
40 print()
41
42 # 5. Demonstrate fetching chat history
43 print("--- Fetching Chat Histories ---")
44
45 # History of Alice and Bob's chat
46 print(f"\nHistory for chat '{alice_bob_chat.get_name(alice)}':")
47 one_to_one_history = chat_service.print_chat_history(alice_bob_chat.get_id())
48 for message in one_to_one_history:
49 print(message)
50
51 # History of the project group chat
52 print(f"\nHistory for chat '{project_group.get_name(charlie)}':")
53 group_history = chat_service.print_chat_history(project_group.get_id())
54 for message in group_history:
55 print(message)
56
57 # 6. Demonstrate finding all of a user's chats
58 print("\n--- Fetching all of Alice's chats ---")
59 alice_chats = chat_service.get_user_chats(alice.get_id())
60 for chat in alice_chats:
61 print(f"Chat: {chat.get_name(alice)} (ID: {chat.get_id()})")
62
63
64if __name__ == "__main__":
65 ChatApplicationDemo.main()Which core entity is responsible for creating new chats and managing chat-level operations in a chat application?
No comments yet. Be the first to comment!